#include <QDebug>
#include <QStringList>
#include "BinaryModule.h"
#include <QVariantList>
#include <QTemporaryFile>
#include "CramBuilder.h"

#define PyAPI_FUNC(RTYPE) RTYPE
#define PyAPI_DATA(RTYPE) extern RTYPE
#include "object.h"

typedef struct PyMemberDef {
        /* Current version, use this */
        char *name;
        int type;
        Py_ssize_t offset;
        int flags;
        char *doc;
} PyMemberDef;


typedef void (*PyInitFunc)();
typedef char* (*PyBytes_AsStringFunc)(PyObject*);
typedef PyObject* (*PyUnicode_AsEncodedStringFunc)(PyObject*, const char*, const char*);


BinaryModule::BinaryModule()
{
    _lib.setFileName("/usr/lib/libpython3.so");
    _lib.load();
    PyInitFunc init = (PyInitFunc)_lib.resolve("Py_Initialize");
    _pyBytes_AsString = (PyBytes_AsStringFunc)_lib.resolve("PyBytes_AsString");
    _pyUnicode_AsEncodedString = (PyUnicode_AsEncodedStringFunc)_lib.resolve("PyUnicodeUCS4_AsUTF8String");
    _pyDictNext = (DictNextFunc)_lib.resolve("PyDict_Next");
    _pyImportModule = (ModInfoFunc)_lib.resolve("PyImport_ImportModule");
    _pyGetDict = (PyWArgFunc)_lib.resolve("PyModule_GetDict");
    _pyAttrString = (GetAttr)_lib.resolve("PyObject_GetAttrString");
    _pyAttr = (GetAttr)_lib.resolve("PyObject_GetAttr");
    _pyDir = (PyWArgFunc)_lib.resolve("PyObject_Dir");
    _pyListGetItem = (PyList_GetItemFunc)_lib.resolve("PyList_GetItem");
    _pySeqGetItem = (PyList_GetItemFunc)_lib.resolve("PySequence_GetItem");
    _pyHasAttr = (PyObject_HasAttrStringFunc)_lib.resolve("PyObject_HasAttrString");

    if (init)
        init();
}

BinaryModule::~BinaryModule()
{
    PyInitFunc deinit = (PyInitFunc)_lib.resolve("Py_Finalize");
    if (deinit)
        deinit();
    _lib.unload();
}

QStringList BinaryModule::importPathes()
{
    /*typedef PyObject* (*PyRun_StringFunc)(const char*, int , PyObject*, PyObject*);
    PyRun_StringFunc pyRun = (PyRun_StringFunc)_lib.resolve("PyRun_String");

    QString out = asString(pyRun("import sys\nprint(sys.path)", 0, NULL, NULL));
    qDebug() << out;*/
    QStringList imports;
    PyObject* mod = importModule("sys");
    PyObject* imp = attr(mod, "path");
    for(int i = 0; i < Py_SIZE(imp); i++){
        PyObject* item = _pyListGetItem(imp, i);
        imports.append(asString(item));
        //Py_XDECREF(item);
    }
    Py_XDECREF(imp);
    Py_XDECREF(mod);
    imports.sort();
    return imports;
}

QString BinaryModule::asString(PyObject* str)
{

    PyObject* bytes = _pyUnicode_AsEncodedString(str, "utf-8", "Error");
    if (bytes){
        QString res = _pyBytes_AsString(bytes);
        Py_XDECREF(bytes);
        return res;
    }
    return "";
}

QMap<QString, PyObject*> BinaryModule::dir(PyObject* what)
{
    QMap<QString, PyObject*> ret;

    PyObject* dict = _pyDir(what);
    if (!dict)
        return ret;
    for(Py_ssize_t i = 0; i < Py_SIZE(dict); ++i){
        PyObject* item = _pyListGetItem(dict, i);
        QString name = asString(item);
        ret[name] = attr(what, name);
        //qDebug() << name;
    }
    Py_XDECREF(dict);
    return ret;
}

PyObject* BinaryModule::importModule(const QString& modName)
{
    qDebug() << "Binary import "<< modName;
    return _pyImportModule(qPrintable(modName));
}

QString BinaryModule::attrString(PyObject* obj, const QString& attrName)
{
    if (obj && _pyHasAttr(obj, qPrintable(attrName))){
        PyObject* strObj = _pyAttrString(obj, qPrintable(attrName));
        if (strObj){
            QString res = asString(strObj);
            Py_DECREF(strObj);
            return res;
        }
    }
    return "";
}

PyObject* BinaryModule::attr(PyObject* obj, const QString& attrName)
{
    if (_pyHasAttr(obj, qPrintable(attrName)))
        return _pyAttrString(obj, qPrintable(attrName));
    return NULL;
}

QString BinaryModule::typeName(PyObject* o)
{
    return o->ob_type->tp_name;
}

QStringList BinaryModule::getMro(PyObject* o)
{
    PyObject* mro = attr(o, "__bases__");
    QStringList out;
    for(Py_ssize_t i = 0; i < Py_SIZE(mro); ++i){
        PyObject* item = _pySeqGetItem(mro, i);
        if (item){
            if (item != o)
                out << attrString(item, "__name__");
            Py_DECREF(item);
        }
    }
    Py_XDECREF(mro);
    return out.length() ? out : out << "object";
}

QString BinaryModule::getDoc(PyObject* o)
{
    QString ret;
    PyObject* at = attr(o, "__doc__");
    if (!strcmp(at->ob_type->tp_name, "str"))
        ret = asString(at);
    Py_XDECREF(at);
    return ret;
}

QStringList BinaryModule::funcArgs(PyObject* o, const QString& funcName)
{
    QStringList out;
    PyObject* code = attr(o, "__code__");
    if (code){
        qDebug() << Py_SIZE(code) << code->ob_type->tp_name;
        for(Py_ssize_t i = 0; i < Py_SIZE(code); ++i){
            PyObject* item = _pySeqGetItem(code, i);
            qDebug() << item;
        }
    } else {
        QString doc = getDoc(o);
        QRegExp exp(QRegExp::escape(funcName)+"\\((.*)\\)\\s+(\\->)?");
        exp.setMinimal(true);
        if (exp.indexIn(doc) != -1){
            QString argString = exp.cap(1);
            //argString = argString.replace("[", "");
            //argString = argString.replace("]", "");

            if (argString.contains("..."))
                argString = argString.replace("...", "*args");

            QStringList cargs;
            foreach(QString arg, argString.split(",")){
                QStringList parg = arg.trimmed().split("=");
                if (parg.length() > 1){
                    QString narg = parg[0].replace("[", "").replace("]", "");
                    QString param = parg[1];
                    if (param.contains("]") && !param.contains("["))
                        param = param.replace("]", "");
                    cargs.append(narg+"="+param);
                } else {
                    cargs.append(parg[0].replace("-", "_").replace("[", "").replace("]", ""));
                }
            }

            //qDebug() << argString;
            out << cargs.join(", ");
        }
    }

    return out;
}

QString BinaryModule::funcReturn(PyObject* o, const QString& funcName)
{
    QString doc = getDoc(o);
    QRegExp exp(QRegExp::escape(funcName)+"\\((.*)\\)\\s+\\->\\s*(\\w+)[\\W]+");
    exp.setMinimal(true);
    if (exp.indexIn(doc) != -1){
        return parseType(exp.cap(2).trimmed());
    }

    return "";
}

QString BinaryModule::parseType(const QString& type)
{
    if (type == "dictionary" || type == "dict")
        return "dict";
    if (type == "list")
        return "list";
    if (type == "integer" || type == "int")
        return "int";
    if (type == "string" || type == "str" || type == "unicode")
        return "str";
    if (type == "None")
        return "None";
    return "object";
}

void BinaryModule::free(const QMap<QString, PyObject*>& list)
{
    foreach(PyObject* obj, list)
        Py_XDECREF(obj);
    //qDeleteAll(list);
}

void BinaryModule::free(PyObject* obj)
{
    Py_XDECREF(obj);
}

QString BinaryModule::import(const QString& importName)
{
    CramModule boot(importName);

    PyObject* mod = importModule(importName);
    processScope(mod, &boot);
    QTemporaryFile file;
    file.setAutoRemove(false);
    if (file.open()){
        file.write(boot.build().toUtf8());
        file.close();
    }
    free(mod);
    return file.fileName();
}

void BinaryModule::processScope(PyObject* obj, CramBuilder* builder, bool onlyPublic)
{
    QMap<QString, PyObject*> info = dir(obj);
    foreach(QString key, info.keys()){
        if (!info[key])
            continue;

        QString type = typeName(info[key]);
        if (type == "str"){
            if (onlyPublic && key.startsWith("__"))
                continue;
            QString doc = attrString(obj, key);
            if (key == "__doc__" || doc.split("\n").length() > 1)
                *builder << builder->ident << key << "=\"\"\"" << doc << "\"\"\"\n";
            else
                *builder << builder->ident << key << "=\"" << doc << "\"\n";
        } else if (type == "module"){
            if (key != "__builtins__" && key != "builtins")
                *builder << builder->ident << "import " << key;
        } else if (type == "type"){
            if (onlyPublic && key.startsWith("__"))
                continue;
            CramClass *cls = builder->addClass(key, getMro(info[key]), getDoc(info[key]));
            processScope(info[key], cls, true);
        } else if (type == "function" || type == "method_descriptor" || type == "builtin_function_or_method"){
            builder->addFunction(key, funcArgs(info[key], key), getDoc(info[key]), funcReturn(info[key], key));
        }
    }
    free(info);
}
